
import com.mysql.jdbc.Connection;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.sql.DriverManager;
import java.util.HashMap;
import java.util.Properties;

/**
 * This class represents the main controller of the server. His function is to receive
 * and store (in a database) measures sent by clients all over the network. The server also
 * has a second role, which is to retrieve (in a database) orders (like shutdown or reboot)
 * given by network administrators (from the web interface).
 * <p>
 * The server is fault-tolerant. For example, if clients sended measures, and the database
 * crashes, those measures will be kept in memory until the database system is operationnal
 * again.
 *
 * @author Alexandre Dos Santos Coelho
 */
public class Server extends Thread {

  private final String configFile = "server.conf"; // Config file path
  private int listenPort; // Server listening port for incoming connexions
  private String dbAddress; // Database server address
  private int dbPort; // Database server port
  private String dbUser; // Database server username
  private String dbPass; // Database server password
  private String dbName; // Database server database name
  private Connection dbConnection; // Database connection
  private int orderDelay; // Delay between two queries on the Order table
  private boolean databaseConnected = false; // Determines if the server is connected to the DB
  private ServerSocket socket; // Socket for new connections
  private HashMap<String, Client> clients; // Connected clients
  private boolean running = true; // Determines if the server is running or should shutdown
  private DataManager dataManager; // Used to store received measures from clients in DB
  private OrderThread orderThread; // Used to get orders sent from admin to connected machines

  /**
   * Constructor. Launches the servers with the configuration parameters specified in
   * the "server.conf" file.
   */
  public Server() {
    loadConfig(); // Load server configuration parameters

    try {
      // Open server socket for new connections
      socket = new ServerSocket(listenPort);
      connectDB(); // Connection to database server
    } catch (IOException ex) {
      System.err.println(Logger.getTime() + "Impossible to open socket on port: " + listenPort);
      System.err.println("Cause: " + ex.getMessage());
      System.exit(-1);
    }

    clients = new HashMap<String, Client>();
    dataManager = new DataManager(this);
    orderThread = new OrderThread(this, orderDelay);
    this.start();
  }

  /**
   * Removes a client from the connected clients list
   * @param client Client to remove from connected clients list
   */
  public void removeClient(Client client) {
    clients.remove(client);
  }

  /**
   * Checks if a client with a specified hostname is actually connected to the server
   * @param machineId hostname of searched client
   * @return if the client is actually connected
   */
  public boolean clientExists(String machineId) {
    return clients.get(machineId) != null;
  }

  /**
   * Returns the client with the corresponding hostname. Returns null if client is not connected.
   * @param machineId client hostname
   * @return searched client
   */
  public Client getClient(String machineId) {
    return clients.get(machineId);
  }

  /**
   * Adds a client to the currently connected clients list. This is used after a client has
   * authenticated in the Client class.
   * @param machineId Client hostname
   * @param c Concerned client
   */
  public void addClient(String machineId, Client c) {
    clients.put(machineId, c);
  }

  /**
   * loads the server's configuration
   */
  private void loadConfig() {
    Properties config = new Properties();

    // Open configuration file
    try {
      config.load(new FileInputStream(configFile));
    } catch (IOException ex) {
      System.err.println(Logger.getTime() + "Impossible to open configuration file!");
    }

    // Get configuration elements
    dbAddress = (String) config.get("server");
    dbPort = Integer.valueOf((String) config.get("port"));
    dbUser = (String) config.get("user");
    dbPass = (String) config.get("pass");
    dbName = (String) config.get("base");
    listenPort = Integer.valueOf((String) config.get("listenPort"));
    orderDelay = Integer.valueOf((String) config.get("orderDelay"));
  }

  /**
   * Thread run method. Opens the server ports and accepts new client connexions.
   */
  public void run() {
    System.out.println(Logger.getTime() + "Server started on port " + listenPort);

    while (running) {
      try {
        Socket client = socket.accept(); // Waiting for new connection
        Client c = new Client(client, this); // Creating new connexion
      } catch (IOException ex) {
        System.err.println(Logger.getTime() + "Socket error: " + ex.getMessage());
      }
    }

  }

  /**
   * Connects to the database with the specified parameters in the "server.conf" file
   */
  private void connectDB() {
    if (!isDatabaseConnected()) {
      try {
        Class.forName("com.mysql.jdbc.Driver"); // loading Mysql JDBC driver
        String url = "jdbc:mysql://" + dbAddress + ":" + dbPort + "/" + dbName; // mysql connection string
        dbConnection = (Connection) DriverManager.getConnection(url, dbUser, dbPass);
        setDatabaseConnected(true);
      } catch (Exception e) {
        System.err.println(Logger.getTime() + "Impossible to connect to the database");
        try {
          Thread.sleep(10000);
        } catch (InterruptedException ex) {
        }
        System.err.println(Logger.getTime() + "Trying to reconnect to the database");
        connectDB();
      }
    }
  }

  /**
   * Attempts to reconnect to the database every 2 seconds
   */
  public void reconnectDB() {
    new Thread() {

      public void run() {
        while (!isDatabaseConnected()) {
          connectDB();
          try {
            Thread.sleep(10000);
          } catch (InterruptedException ex) {
          }
        }
      }
    }.start();
  }

  /**
   * Returns the DataManager, who handles database access
   * @return the DataManager
   */
  public DataManager getDataManager() {
    return dataManager;
  }

  /**
   * Returns the connection to the database
   * @return the database connection
   */
  public Connection getDbConnection() {
    return dbConnection;
  }

  /**
   * Returns a flag which indicates if the server is actually connected to the database
   *
   * @return database connection state
   */
  public boolean isDatabaseConnected() {
    return databaseConnected;
  }

  /**
   * Sets the flag which indicates if the server is acutally connected to the database
   * @param databaseConnected database connection state
   */
  public void setDatabaseConnected(boolean databaseConnected) {
    this.databaseConnected = databaseConnected;
  }

  /**
   * Server main method. Creates and launches an instance of Server.
   * @param args
   */
  public static void main(String... args) {
    new Server();
  }
}
